function Surf(image, balanceValue, threshold, octaves) {
  var HESSIAN_INIT_SAMPLE = 2,
    HESSIAN_BALANCE_VALUE = 0.81,
    HESSIAN_THRESHOLD = 0.0004,
    HESSIAN_OCTAVES = 5;

  this.mOriginalImage = image;
  this.mBalanceValue = balanceValue || HESSIAN_BALANCE_VALUE;
  this.mThreshold = threshold || HESSIAN_THRESHOLD;
  this.mNumOctaves = octaves || HESSIAN_OCTAVES;

  //Calculate the integral image
  const IntegralImage = require('./IntegralImage');
  this.mIntegralImage = new IntegralImage(this.mOriginalImage);

  //Calculate the fast hessian
  const FastHessian = require('./FastHessian');
  this.mHessian = new FastHessian(this.mIntegralImage, this.mNumOctaves, HESSIAN_INIT_SAMPLE, this.mThreshold, this.mBalanceValue);

  //Calculate the descriptor and orientation free interest points
  this.mDescriptorFreeInterestPoints = this.mHessian.getIPoints();

  this.getFreeOrientedInterestPoints = function () {
    return this.getPoints(false);
  }

  this.getPoints = function (upright) {
    var points = upright ? this.mUprightPoints : this.mFreeOrientedPoints;
    if (points == null) {
      points = this.getDescriptorFreeInterestPoints();
      // cache for next time through
      if (upright) {
        this.mUprightPoints = points;
      } else {
        this.mFreeOrientedPoints = points;
      }
      var _this = this
      points.forEach(function (point) {
        _this.getOrientation(point);
        _this.getMDescriptor(point, upright);
      })
    }
    return points;
  }

  this.getDescriptorFreeInterestPoints = function () {
    var points = [];
    this.mDescriptorFreeInterestPoints.forEach(function (point) {
      try {
        points.push(point.clone());
      } catch (e) {
        console.log(e)
      }
    })
    return points;
  }

  this.getOrientation = function (input) {
    var gauss;
    var scale = input.getScale();
    var s = Math.round(scale);
    var r = Math.round(input.getY());
    var c = Math.round(input.getX());
    var xHaarResponses = [];
    var yHaarResponses = [];
    var angles = [];

    // calculate haar responses for points within radius of 6*scale
    for (var i = -6; i <= 6; ++i) {
      for (var j = -6; j <= 6; ++j) {
        if (i * i + j * j < 36) {
          gauss = require('./GaussianConstants').Gauss25[Math.abs(i)][Math.abs(j)];
          var xHaarResponse = gauss * this.haarX(r + j * s, c + i * s, 4 * s);
          var yHaarResponse = gauss * this.haarY(r + j * s, c + i * s, 4 * s);
          xHaarResponses.push(xHaarResponse);
          yHaarResponses.push(yHaarResponse);
          angles.push(this.getAngle(xHaarResponse, yHaarResponse));
        }
      }
    }

    // calculate the dominant direction
    var sumX = 0,
      sumY = 0;
    var ang1,
      ang2,
      ang;
    var max = 0;
    var orientation = 0;

    // loop slides pi/3 window around feature point
    for (ang1 = 0; ang1 < 2 * Math.PI; ang1 += 0.15) {
      ang2 = (ang1 + Math.PI / 3.0 > 2 * Math.PI ? ang1 - 5.0 * Math.PI / 3.0 : ang1 + Math.PI / 3.0);
      sumX = sumY = 0;
      for (var k = 0; k < angles.length; k++) {
        ang = angles[k];
        if (ang1 < ang2 && ang1 < ang && ang < ang2) {
          sumX += xHaarResponses[k];
          sumY += yHaarResponses[k];
        } else if (ang2 < ang1 && ((ang > 0 && ang < ang2) || (ang > ang1 && ang < 2 * Math.PI))) {
          sumX += xHaarResponses[k];
          sumY += yHaarResponses[k];
        }
      }

      // if the vector produced from this window is longer than all
      // previous vectors then this forms the new dominant direction
      if (sumX * sumX + sumY * sumY > max) {
        // store largest orientation
        max = sumX * sumX + sumY * sumY;
        orientation = this.getAngle(sumX, sumY);
      }
    }
    input.setOrientation(orientation);
  }

  this.haarX = function (row, column, s) {
    const ImageTransformUtils = require('./ImageTransformUtils');
    return ImageTransformUtils.BoxIntegral(this.mIntegralImage, row - Math.floor(s / 2), column, s, Math.floor(s / 2)) - 1 * ImageTransformUtils.BoxIntegral(this.mIntegralImage, row - Math.floor(s / 2), column - Math.floor(s / 2), s, Math.floor(s / 2));
  }

  this.haarY = function (row, column, s) {
    const ImageTransformUtils = require('./ImageTransformUtils');
    return ImageTransformUtils.BoxIntegral(this.mIntegralImage, row, column - Math.floor(s / 2), Math.floor(s / 2), s) - 1 * ImageTransformUtils.BoxIntegral(this.mIntegralImage, row - Math.floor(s / 2), column - Math.floor(s / 2), Math.floor(s / 2), s);
  }

  this.getAngle = function (xHaarResponse, yHaarResponse) {
    if (xHaarResponse >= 0 && yHaarResponse >= 0)
      return Math.atan(yHaarResponse / xHaarResponse);

    if (xHaarResponse < 0 && yHaarResponse >= 0)
      return Math.PI - Math.atan(-yHaarResponse / xHaarResponse);

    if (xHaarResponse < 0 && yHaarResponse < 0)
      return Math.PI + Math.atan(yHaarResponse / xHaarResponse);

    if (xHaarResponse >= 0 && yHaarResponse < 0)
      return 2 * Math.PI - Math.atan(-yHaarResponse / xHaarResponse);

    return 0;
  }

  this.getMDescriptor = function (point, upright) {
    var y,
      x,
      count = 0;
    var sample_x,
      sample_y;
    var scale,
      dx,
      dy,
      mdx,
      mdy,
      co = 1.0,
      si = 0.0;
    var desc = new Array(64);
    var gauss_s1 = 0.0,
      gauss_s2 = 0.0,
      xs = 0.0,
      ys = 0.0;
    var rx = 0.0,
      ry = 0.0,
      rrx = 0.0,
      rry = 0.0,
      len = 0.0;
    var i = 0,
      ix = 0,
      j = 0,
      jx = 0;

    var cx = -0.5,
      cy = 0.0; // Subregion centers for the 4x4 gaussian weighting

    scale = point.getScale();
    x = Math.round(point.getX());
    y = Math.round(point.getY());
    if (!upright) {
      co = Math.cos(point.getOrientation());
      si = Math.sin(point.getOrientation());
    }
    i = -8;
    // Calculate descriptor for this interest point
    // Area of size 24 s x 24 s
    // ***********************************************
    while (i < 12) {
      j = -8;
      i = i - 4;

      cx += 1.0;
      cy = -0.5;
      while (j < 12) {
        dx = dy = mdx = mdy = 0.0;
        cy += 1.0;

        j = j - 4;

        ix = i + 5;
        jx = j + 5;

        xs = Math.round(x + (-jx * scale * si + ix * scale * co));
        ys = Math.round(y + (jx * scale * co + ix * scale * si));

        for (var k = i; k < i + 9; ++k) {
          for (var l = j; l < j + 9; ++l) {
            // Get coords of sample point on the rotated axis
            sample_x = Math.round(x + (-1.0 * l * scale * si + k * scale * co));
            sample_y = Math.round(y + (l * scale * co + k * scale * si));

            // Get the gaussian weighted x and y responses
            gauss_s1 = this.gaussian(xs - sample_x, ys - sample_y, 2.5 * scale);

            rx = this.haarX(sample_y, sample_x, Math.floor(2 * Math.round(scale)));
            ry = this.haarY(sample_y, sample_x, Math.floor(2 * Math.round(scale)));

            // Get the gaussian weighted x and y responses on
            // rotated axis
            rrx = gauss_s1 * (-rx * si + ry * co);
            rry = gauss_s1 * (rx * co + ry * si);

            dx += rrx;
            dy += rry;

            mdx += Math.abs(rrx);
            mdy += Math.abs(rry);
          }
        }

        // Add the values to the descriptor vector
        gauss_s2 = this.gaussian(cx - 2.0, cy - 2.0, 1.5);

        // Casting from a double to a float, might be a terrible idea
        // but doubles are expensive
        desc[count++] = dx * gauss_s2;
        desc[count++] = dy * gauss_s2;

        desc[count++] = mdx * gauss_s2;
        desc[count++] = mdy * gauss_s2;

        // Accumulate length for vector normalisation
        len += (dx * dx + dy * dy + mdx * mdx + mdy * mdy) * (gauss_s2 * gauss_s2);

        j += 9;
      }
      i += 9;
    }

    len = Math.sqrt(len);

    for (i = 0; i < 64; i++)
      desc[i] /= len;

    point.setDescriptor(desc);
  }

  this.gaussian = function (x, y, sig) {
    return (1.0 / (2.0 * Math.PI * sig * sig)) * Math.exp(-(x * x + y * y) / (2.0 * sig * sig));
  }

  this.getMatchingPoints = function (descriptor, upright) {
    var matchingPoints = [];
    var points = upright ? this.getUprightInterestPoints() : this.getFreeOrientedInterestPoints();
    points.forEach(function (a) {
      var smallestDistance = Number.MAX_VALUE;
      var nextSmallestDistance = Number.MAX_VALUE;
      var possibleMatch = null;
      var descriptorPoints = upright ? descriptor.getUprightInterestPoints() : descriptor.getFreeOrientedInterestPoints();
      descriptorPoints.forEach(function (b) {
        var distance = a.getDistance(b);
        if (distance < smallestDistance) {
          nextSmallestDistance = smallestDistance;
          smallestDistance = distance;
          possibleMatch = b;
        } else if (distance < nextSmallestDistance) {
          nextSmallestDistance = distance;
        }
      })

      // If match has a d1:d2 ratio < 0.65 ipoints are a match
      if (smallestDistance / nextSmallestDistance < 0.75) {
        // not storing change in position
        matchingPoints.push([a, possibleMatch]);
      }
    })
    return matchingPoints;
  }

  this.getUprightInterestPoints = function () {
    return this.getPoints(true);
  }
}

module.exports = Surf
